#! /usr/bin/perl -w

# Control blinds script
#
# This script is the main control script for the sunblinds.
# It does the following: 
#  * firstly it tries to read the ambient light level from
#    the wireless sensor gateway (that received light level values from the sensor
#    in the garden)
#  * if the gateway is not responding or the sensor in not transmitting 
#    values in the last measurement interval, the script reverts to 
#    'calculated' sunrise/sunset times that are read from an XML file.
#    That file is generated by the 'write_blind_control_setting.pl' script
# 
# Every time the script is run, it compared the 'wanted' state with the last
# commanded state (read from the status file called 'last_command.txt').
#
# It is the idea to call this script once every minute. It is however safe to run the script
# less frequently. This is covered by comparing the most recent command with the command
# stored in the status file. If both commands don't match, an update is sent to the
# blind controller. This also covers cases when e.g. the network or the server went down:
# if sending a command to the blind controller fails, the status file is not updated so 
# that the command will be resent the next time the script is run.
#
# L. Hollevoet, 2007.
use strict;

use XML::Simple;
use IO::Socket;
use Net::hostent; 

# Register ALARM signal
$SIG{ALRM} = sub { die "timeout" };

# Settings
#my $gateway_host = 'rfgate';			# Host on the network that has light level values
my $gateway_host = '192.168.1.24';			# Host on the network that has light level values
my $gateway_port = '10001';				# Port to connect to
my $state_file   = 'last_command.txt';	# File where the last sent command has been sent to
#my $blinds_host  = 'blindnode';			# Host on the network that controls the blinds
my $blinds_host  = '192.168.1.21';			# Host on the network that controls the blinds
my $blinds_port  = '10001';				# Port to connect to on the blind controller
my $debug        = '0';                 # Set to not '0' to print debug messages and to send the command to the blind controller

# Global variables
my $next_command;
my $socket = 0;
my $response = 0;

my $cmd_time = get_current_time();      # Time of day for computation purposes
my $timestamp = date_time();            # For printing purpose, human readible with date


# Check if we need to run, we should not during the night, and we don't want to
# send the blinds up too early in the morning :-)
verify_activation_time($cmd_time);


# First try to get and parse the ambient light level from the garden sensor
$socket = open_socket($gateway_host, $gateway_port); # Open the socket with possibility to retry if the target port is in use (can happen on the Lantronix XPORT)
if ($socket) {
	$socket->send("?\r");
	$response = read_socket(10);

	# Close the socket again, we don't need it anymore
	close_socket();
	
	# If we could not connect, the response is marked as invalid in the sensor_value_valid() function.
}

# Example expired response from light sensor (last byte is FF) for debuggin purposes
#$response = "Node 1: FF 8A 4F FF";

# Determine the command to send, either based on sensor readings (if valid), or on calculated value
if (sensor_value_valid($response)){
	print "Sensor value seems not expired, parsing...\n" if ($debug);
    $next_command = parse_sensor_report($response);
} else {
	print "Sensor value expired, using XML fallback...\n" if ($debug);
    $next_command = determine_calculated_command();
}

# Get the last command we sent to the blind controller
my $last_command = get_last_command($state_file);

print "Last command sent to blinds was '$last_command', next one is '$next_command'\n" if ($debug);

# If the command and the status file don't match, update!
# Unless the $next_command is empty, in case no action is required
if (($next_command ne '') && ($last_command ne $next_command)){
	
	print "$timestamp New command '$next_command' will be sent to blinds based on ";
	if (sensor_value_valid($response)){
		print "sensor";
	} else {
		print "backup file";
	}
	print "\n";
	
	# Send it to the blind controller
	if (command_blinds($next_command)){
		save_last_command($state_file, $next_command);
	}
} else {
	#print "[$current_time] No action taken, blinds already in position\n";
}

exit(0);

############################################################################
# Subroutines
################

#
# Blind controller related
#
sub command_blinds  {
	my $blind_command = shift();
	
	if ($debug) {
		print "     but not actually sent to blinds since debug mode is active...\n";
		return 0;
	}
	
	print "$timestamp Connecting to socket $blinds_host... ";
		
	# Open the socket to send the command
	my $socket = open_socket($blinds_host, $blinds_port);

	# If we got a valid socket
	if ($socket) {
	    print " Connected on port $blinds_port... ";
	
		$socket->send('?');
		sleep(1);
		$blind_command = substr($blind_command, 0, 1);
		$socket->send($blind_command);
	
		print "'$blind_command' sent to host...\n";
		
		sleep(1);
	
		close($socket);

		return 1;						
	
	} else {
		print "Could not open socket\n";
		return 0;
	}

}

#
# Program helper functions
#

## Retrieve the last command sent to the blind controller from this script
sub get_last_command {
	
	# Get filename
	my $filename = shift();
	
	# Default assignment of the last command
	my $last_command = '?';
	if (open (STATUS, $filename) ){
		$last_command = <STATUS>;
		chomp($last_command) if defined ($last_command);
	} else {
		print "Could not open last command file, creating new one...\n";
	};
	
	close(STATUS);
	
	$last_command = '' if (!defined($last_command));
	
	return $last_command;
}

## Save the last command sent to the blind controller from this script
sub save_last_command {
	
	# Get filename and last command
	my $filename = shift();
	my $last_command = shift();
	
	open (STATUS, ">", $filename) or (die "Could not open state file for output: $!");
	
	print STATUS "$last_command";
	
	close (STATUS);
	
	return;
}	

## The script will exit in this funtion is the time is between midnight and 6:30 am.
sub verify_activation_time {
	my $time = shift();
	
	if ($time =~ /(\d+):(\d+)/){
		my $hr = $1; # Current hour
		my $mn = $2; # Current minute
		if ($hr < 6) {
			print "Time is before 6 a.m., exit!\n" if ($debug);
			exit(0);
		} elsif ($hr == 6 && $mn < 30) {
			print "Time is before 6.30 a.m., exit!\n" if ($debug);
			exit(0);
		}
	}
}

######## Wireless gateway related routines



###############################################################
## handle_timeout
#   Close everything and exit if we have a timeout
###############################################################
sub handle_timeout {
    &close_socket();
    &error("Timeout while waiting for data from host! Check the server\n");
}




###############################################################
## clean_and_exit
#   Shutdown cleanly
###############################################################
sub clean_and_exit {
    &close_socket();    
    exit 0;
}

###############################################################
## error
#   Signal error and exit
###############################################################
sub error {
    my $data = shift();
    print $data;
    &close_socket();
    exit 1;
}

###############################################################
## close_socket
#   Close the socket
###############################################################
sub close_socket {
    if (defined $socket){
	$socket->close();
    }    
}

###############################################################
## open_socket(host, port)
#   Try to open a TCP session to 'host' on 'port'.
#   Retry if the server is not available, timeout after 
#   10 tries with 1 second sleep.
###############################################################
sub open_socket {

	# Read input parameters
    my $client_host = shift(); 
    my $client_port = shift();

	my $socket = 0;
	
    # Connect to the remote device 
    # create a tcp connection to the specified host and port
    my ($kidpid, $handle, $line);

    my $timeout = 0;
	
	my $cmd_time = get_current_time();
	
	print "Opening socket to $client_host" . "[" . "$client_port]\n" if ($debug);
	
    while () {
        if ($socket = IO::Socket::INET->new(Proto     => "tcp",
					PeerAddr  => $client_host,
					PeerPort  => $client_port,
					Type      => SOCK_STREAM)){
	   		last;
        } else {
	   		print "$timestamp Waiting for server '$client_host' to become available, timeout 10 seconds\n" if ($timeout == 0);
	   		print ".";
	   		$timeout++;
	   		if ($timeout == 10){
	     		print "\nServer not available, returning 0...\n";
	     		return 0;
	   		}
	   
	  		sleep 1;
        }
    }
    
    return $socket;
    
}

###############################################################
## read_socket(timeout)
#   Reads data from the socket interface. Times out if nothing is
#   received after <timeout> seconds.
###############################################################
sub read_socket {
    my $timeout = shift();

    my @numresult;
    my $result;

    eval {
	# Set alarm
	alarm($timeout);

	# Execute receive code
	my $waiting = 1;
	$result = "";
	my $res = "";
	while ($waiting){

	    # Read reply
	    ($socket->recv($res, 32));
	    $result .= $res;
	    if (($result =~ /EOT/)) {
		$waiting = 0;
	    }
	}
	
	# Clear alarm
	alarm(0);
    };

    # Check what happened in the eval loop
    if ($@) {
	if ($@ =~ /timeout/) {
	    # Oops, we had a timeout
	    print "Received up to now: $result";
		close_socket();
		die "Response not received in time! Exit...";
	} else {
	    # Oops, we died
	    alarm(0);           # clear the still-pending alarm
	    die;                # propagate unexpected exception
	} 
	
    } 

    # We get here if the eval exited normally
    #@numresult = &parse_response($result);
	return $result;
	
}

sub sensor_value_valid {
	my $data = shift();
	my $result_age;
	
	if ($data =~ /Node 1: [0-9A-F]{2} [0-9A-F]{2} [0-9A-F]{2} ([0-9A-F]{2})/) { $result_age = $1;} else { $result_age = 'FF'};
	
	# Convert to decimal
	my $age_dec = hex($result_age);
	
	
	# If age < 0xF0 then we assume we have a 'fresh' report waiting
	if ($age_dec < 0xF0){
		return 1;
	} else {
	    #print "Sensor report age: " . $age_dec . " returning zero\n";
		return 0;
	}
	
}

###############################################################
## parse_and_report(response)
# Parse the returned string and determine the next command
###############################################################
sub parse_sensor_report {

	my $data = shift();
	
	my $hex1;
	
	# Extract values
	if ($data =~ /Node 1: ([0-9A-F]{2}) /) { $hex1 = $1}
	
	# Convert to decimal
	my $solar = hex($hex1);

	my $command = '';
	
	# If level goes > 20 and it is morning: send first up
	#               > 80 and it is morming: send second up
	#               < 15 and it is evening: send down
	
	my ($sec, $min, $hour) = localtime(time);
	
	if ($hour < 12) {
		if ($solar > 20 && $solar < 80){
			$command = 'u1';
		} elsif ( $solar >= 80 ) {
			# In the summer, $solar will already be > 80 when the script is running the first time. So if we get here
			# and time < 06:35, then we send u1 first.
			if ($min < 35 && $hour == 6){
				$command = 'u1';
			} else {
				$command = 'u2';
			}
		}
	} else {
		if ( $solar <= 30 ) { 
			$command = 'd';
		}
	}

	return $command;
}

###############################################################
## determine_calculated_command()
# Get the command that should be sent according to the 
# calculation script, only used as a fallback
###############################################################
sub determine_calculated_command {

	# Read the XML config file
	my $xml = new XML::Simple(keyattr => ['time']);

	my $config = $xml->XMLin("blind_settings_today.xml");

    my $current_time = get_current_time();
    
	# Determine the command that is valid now according to the script
	my $lastmatch;

	foreach (sort keys %{$config->{command}}){
		my $compare_time = $_;
		# Determine the last command
		if ($current_time ge $compare_time){
			$lastmatch = $compare_time;		
		}
	}
 
	# Get the actual command of the last programmed command (up or down)
	my $lastmatch_command;
	if (defined($config->{command}->{$lastmatch}->{direction})){
		$lastmatch_command = $config->{command}->{$lastmatch}->{direction};
	} else {
		$lastmatch_command = '';
	}
	
	print "Calculated direction to send: $lastmatch_command\n" if ($debug);
	return $lastmatch_command;
}

sub get_current_time {
	
       # Get the local time
        my ($sec, $min, $hour) = localtime(time);

        # Make sure to append a '0' if required, this is done so that the
        # time is easily readible/comparable
        my $minutes = $min > 9 ? "$min" : "0$min"; 
        my $hours = $hour > 9 ? "$hour" : "0$hour";

        my $current_time = $hours . ':' . $minutes;
        
        return $current_time;
	
}

sub date_time {

	# Get localtime	
    my ($second, $minute, $hour, $dayOfMonth, $month, $yearOffset, $dayOfWeek, $dayOfYear, $daylightSavings) = localtime();
    my $year = 1900 + $yearOffset;
    # Convert to nicely formatted strings
    my $hms  = sprintf("%02i:%02i:%02i", $hour, $minute, $second);
    my $ymd  = sprintf("%04i%02i%02i", $year, $month, $dayOfMonth);
    my $theTime = "[$ymd $hms]";
    return $theTime;
	
}
